diff --git a/src/chain.h b/src/chain.h index fd54a131e..69c66c674 100644 --- a/src/chain.h +++ b/src/chain.h @@ -38,6 +38,12 @@ extern bool fZindex; // to 0 after node restart. DragonX CLIENT_VERSION is 1000350 (v1.0.3.50). static const int SPROUT_VALUE_VERSION = 1000000; static const int SAPLING_VALUE_VERSION = 1000000; +// Block-index records written at >= this version store nSaplingValue as a boost::optional +// (1-byte discriminant + value). Earlier records stored it as a raw 8-byte CAmount; the +// deserializer consumes those bytes but reads the value as boost::none (untrusted) so the +// turnstile guard stays dormant until such a node reindexes. Keep == the CLIENT_VERSION that +// introduced the optional format (v1.0.3, CLIENT_VERSION 1000350). +static const int SAPLING_VALUE_OPTIONAL_VERSION = 1000350; extern int32_t ASSETCHAINS_LWMAPOS; extern char SMART_CHAIN_SYMBOL[65]; extern uint64_t ASSETCHAINS_NOTARY_PAY[]; @@ -373,10 +379,10 @@ public: //! Will be boost::none if nChainTx is zero. boost::optional nChainSproutValue; - //! Change in value held by the Sapling circuit over this block. - //! Not a boost::optional because this was added before Sapling activated, so we can - //! rely on the invariant that every block before this was added had nSaplingValue = 0. - CAmount nSaplingValue; + //! Change in value held by the Sapling circuit over this block. boost::none for blocks + //! before nSaplingValue was tracked, or on nodes that loaded an older-format block index + //! (see SAPLING_VALUE_OPTIONAL_VERSION) -- propagates to nChainSaplingValue == none. + boost::optional nSaplingValue; //! (memory only) Total value held by the Sapling circuit up to and including this block. //! Will be boost::none if nChainTx is zero. @@ -460,7 +466,7 @@ public: nSequenceId = 0; nSproutValue = boost::none; nChainSproutValue = boost::none; - nSaplingValue = 0; + nSaplingValue = boost::none; nChainSaplingValue = boost::none; nVersion = 0; @@ -667,10 +673,22 @@ public: READWRITE(nSproutValue); } - // Only read/write nSaplingValue if the client version used to create - // this index was storing them. - if ((s.GetType() & SER_DISK) && (nVersion >= SAPLING_VALUE_VERSION)) { - READWRITE(nSaplingValue); + // nSaplingValue is a boost::optional so "not reliably tracked" reads back as none, + // keeping the turnstile guard dormant on old/snapshot-bootstrapped DBs. Records written + // before SAPLING_VALUE_OPTIONAL_VERSION stored it as a raw 8-byte CAmount: consume those + // bytes for alignment but DISCARD the value (read as none), since it may be understated on + // a node that never tracked the pool from genesis. That node stays dormant until it + // reindexes, which rewrites records at the current version -> the optional path below. + if (s.GetType() & SER_DISK) { + if (nVersion >= SAPLING_VALUE_OPTIONAL_VERSION) { + READWRITE(nSaplingValue); // new format: boost::optional + } else if (nVersion >= SAPLING_VALUE_VERSION) { + CAmount nLegacySaplingValue = 0; // old format: raw 8 bytes present in the stream + READWRITE(nLegacySaplingValue); // consume for alignment; value is discarded + if (ser_action.ForRead()) + nSaplingValue = boost::none; + } + // else (< SAPLING_VALUE_VERSION): field was never stored -> nSaplingValue stays none } // These values only serialized when -zindex enabled diff --git a/src/main.cpp b/src/main.cpp index 74187645a..510fe6f18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3543,6 +3543,19 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin error("ConnectBlock(): block's hashFinalSaplingRoot is incorrect"), REJECT_INVALID, "bad-sapling-root-in-block"); } + // Turnstile / inflation guard (belt-and-suspenders to the per-tx binding-signature check): + // the cumulative Sapling value pool must never go negative -- a block cannot deshield more + // value than was ever shielded. Enforced ONLY when the pool is reliably tracked: pprev's + // nChainSaplingValue is engaged only if every ancestor since genesis had a known per-block + // value (nSaplingValue). On old/snapshot-bootstrapped nodes it is none -> guard dormant + // (until reindex), so this can never false-reject a valid block or split the chain. + if (pindex->pprev && pindex->pprev->nChainSaplingValue) { + CAmount blockSaplingValue = 0; + for (const CTransaction& btx : block.vtx) + blockSaplingValue += -btx.valueBalance; + if (*pindex->pprev->nChainSaplingValue + blockSaplingValue < 0) + return state.DoS(100, error("ConnectBlock(): Sapling value pool would go negative (turnstile/inflation violation)"), REJECT_INVALID, "bad-sapling-value-pool-negative"); + } } int64_t nTime1 = GetTimeMicros(); nTimeConnect += nTime1 - nTimeStart; LogPrint("bench", " - Connect %u transactions: %.2fms (%.3fms/tx, %.3fms/txin) [%.2fs]\n", (unsigned)block.vtx.size(), 0.001 * (nTime1 - nTimeStart), 0.001 * (nTime1 - nTimeStart) / block.vtx.size(), nInputs <= 1 ? 0 : 0.001 * (nTime1 - nTimeStart) / (nInputs-1), nTimeConnect * 0.000001); @@ -4825,8 +4838,8 @@ bool ReceivedBlockTransactions(const CBlock &block, CValidationState& state, CBl } else { pindex->nChainSproutValue = boost::none; } - if (pindex->pprev->nChainSaplingValue) { - pindex->nChainSaplingValue = *pindex->pprev->nChainSaplingValue + pindex->nSaplingValue; + if (pindex->pprev->nChainSaplingValue && pindex->nSaplingValue) { + pindex->nChainSaplingValue = *pindex->pprev->nChainSaplingValue + *pindex->nSaplingValue; } else { pindex->nChainSaplingValue = boost::none; } @@ -5970,8 +5983,8 @@ bool static LoadBlockIndexDB() } else { pindex->nChainSproutValue = boost::none; } - if (pindex->pprev->nChainSaplingValue) { - pindex->nChainSaplingValue = *pindex->pprev->nChainSaplingValue + pindex->nSaplingValue; + if (pindex->pprev->nChainSaplingValue && pindex->nSaplingValue) { + pindex->nChainSaplingValue = *pindex->pprev->nChainSaplingValue + *pindex->nSaplingValue; } else { pindex->nChainSaplingValue = boost::none; } @@ -6347,7 +6360,7 @@ bool RewindBlockIndex(const CChainParams& params, bool& clearWitnessCaches) pindexIter->nChainTx = 0; pindexIter->nSproutValue = boost::none; pindexIter->nChainSproutValue = boost::none; - pindexIter->nSaplingValue = 0; + pindexIter->nSaplingValue = boost::none; pindexIter->nChainSaplingValue = boost::none; pindexIter->nSequenceId = 0;