diff options
| author | Bitflash <btf01@proton.me> | 2026-07-28 13:58:43 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-28 13:58:43 -0300 |
| commit | 0ddcf690bf48075927f9d7596c62ddb41e79b2e0 (patch) | |
| tree | a9a97ae83a52b449047d070b1b333d5d15c67490 /src | |
| parent | b8b6efd7b4fcaf692a276ac4e4e08fb2b0e3bfc0 (diff) | |
Security: anyone can spend anyone's coins, plus three more consensus bugs (#12)
* script: fix OP_RETURN spend bypass, disable unbounded opcodes
OP_RETURN let anyone spend anyone else's outputs.
VerifySignature evaluates scriptSig and scriptPubKey as one concatenated
script. OP_RETURN was implemented as 'pc = pend', which jumps past
everything that follows, and EvalScript ends with
CastToBool(stack.back()). So a scriptSig of <OP_TRUE OP_RETURN> skipped
the entire scriptPubKey -- OP_CHECKSIG included -- and the final check
read the attacker's own OP_TRUE. No signature required, any output
spendable. This is the 0.1.0 behaviour Bitcoin fixed in 0.3.5.
OP_RETURN now fails the script outright.
Also disables the opcodes Bitcoin removed for being unbounded, and caps
pushed elements at 520 bytes:
OP_CAT grows a stack element with no limit -- <x> OP_DUP OP_CAT
repeated doubles it each time, so a script that fits in a transaction
exhausts memory on every node that validates it. OP_LSHIFT shifts by
an attacker-supplied amount with no ceiling, same effect. There were
no element, stack or opcode-count limits of any kind.
Nothing on this chain uses these opcodes -- all outputs are standard
pay-to-pubkey-hash or coinbase pay-to-pubkey -- so no existing
transaction changes validity.
* main: reject blocks containing a duplicate transaction (CVE-2012-2459)
BuildMerkleTree duplicates the trailing hash whenever a tree level has
an odd width. Appending a copy of the last transaction therefore
produces a block whose merkle root -- and whose block hash -- match a
legitimate block exactly.
A node handed the forgery rejects it (the duplicated inputs are already
spent) and remembers that hash as bad. When the genuine block arrives
carrying the same hash, it is refused too, and the node stops following
the real chain.
CheckBlock now rejects any block that carries the same transaction
twice, which is what the mutation requires.
* main: halve the subsidy on the block's height, not the node's tip
GetBlockValue derived the halving from nBestHeight -- the validating
node's own chain tip -- rather than from the height of the block being
valued. The subsidy a block may pay is a property of that block, so
reading it off local state makes validity depend on how far the node has
synced: across a halving boundary a node still catching up would allow
50 BTF where a synced node allows 25, and the two would follow different
chains. Reorgs have the same problem in miniature.
Harmless today (every node computes nHeight/210000 == 0 at height ~2400)
and it changes no existing block's validity, but it splits the network
at height 210000.
The height is now passed in explicitly: pindex->nHeight when connecting,
tip + 1 when mining or building pool work.
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.cpp | 30 | ||||
| -rw-r--r-- | src/main.h | 2 | ||||
| -rw-r--r-- | src/rpc.cpp | 2 | ||||
| -rw-r--r-- | src/script.cpp | 31 | ||||
| -rw-r--r-- | src/script.h | 4 |
5 files changed, 60 insertions, 9 deletions
diff --git a/src/main.cpp b/src/main.cpp index aff7bab..a7298ea 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -729,12 +729,16 @@ uint256 GetOrphanRoot(const CBlock* pblock) | |||
| 729 | return pblock->GetHash(); | 729 | return pblock->GetHash(); |
| 730 | } | 730 | } |
| 731 | 731 | ||
| 732 | int64 CBlock::GetBlockValue(int64 nFees) const | 732 | int64 CBlock::GetBlockValue(int nHeight, int64 nFees) const |
| 733 | { | 733 | { |
| 734 | int64 nSubsidy = 50 * COIN; | 734 | int64 nSubsidy = 50 * COIN; |
| 735 | 735 | ||
| 736 | // Subsidy is cut in half every 4 years | 736 | // Halve on the height of this block, never on nBestHeight. The subsidy a |
| 737 | nSubsidy >>= (nBestHeight / 210000); | 737 | // block is allowed to pay is a property of that block; deriving it from |
| 738 | // the validating node's own tip makes the answer depend on how far that | ||
| 739 | // node happens to have synced, so two nodes at different heights would | ||
| 740 | // disagree about the same block across a halving boundary. | ||
| 741 | nSubsidy >>= (nHeight / 210000); | ||
| 738 | 742 | ||
| 739 | return nSubsidy + nFees; | 743 | return nSubsidy + nFees; |
| 740 | } | 744 | } |
| @@ -1011,7 +1015,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex) | |||
| 1011 | return false; | 1015 | return false; |
| 1012 | } | 1016 | } |
| 1013 | 1017 | ||
| 1014 | if (vtx[0].GetValueOut() > GetBlockValue(nFees)) | 1018 | if (vtx[0].GetValueOut() > GetBlockValue(pindex->nHeight, nFees)) |
| 1015 | return false; | 1019 | return false; |
| 1016 | 1020 | ||
| 1017 | // Update block index on disk without changing it in memory. | 1021 | // Update block index on disk without changing it in memory. |
| @@ -1237,6 +1241,22 @@ bool CBlock::CheckBlock() const | |||
| 1237 | if (!tx.CheckTransaction()) | 1241 | if (!tx.CheckTransaction()) |
| 1238 | return error("CheckBlock() : CheckTransaction failed"); | 1242 | return error("CheckBlock() : CheckTransaction failed"); |
| 1239 | 1243 | ||
| 1244 | // Reject blocks carrying the same transaction twice (CVE-2012-2459). | ||
| 1245 | // | ||
| 1246 | // BuildMerkleTree duplicates the last hash when a level has an odd width, | ||
| 1247 | // so appending a copy of the trailing transaction(s) yields a block with | ||
| 1248 | // an identical merkle root -- and therefore an identical block hash -- to | ||
| 1249 | // a legitimate one. The forgery fails validation, the hash gets recorded | ||
| 1250 | // as bad, and the real block is then refused because it hashes the same. | ||
| 1251 | set<uint256> setTxHashes; | ||
| 1252 | foreach(const CTransaction& tx, vtx) | ||
| 1253 | { | ||
| 1254 | uint256 hashTx = tx.GetHash(); | ||
| 1255 | if (setTxHashes.count(hashTx)) | ||
| 1256 | return error("CheckBlock() : duplicate transaction in block"); | ||
| 1257 | setTxHashes.insert(hashTx); | ||
| 1258 | } | ||
| 1259 | |||
| 1240 | // Check proof of work matches claimed amount (memory-hard RandomX PoW) | 1260 | // Check proof of work matches claimed amount (memory-hard RandomX PoW) |
| 1241 | if (CBigNum().SetCompact(nBits) > bnProofOfWorkLimit) | 1261 | if (CBigNum().SetCompact(nBits) > bnProofOfWorkLimit) |
| 1242 | return error("CheckBlock() : nBits below minimum work"); | 1262 | return error("CheckBlock() : nBits below minimum work"); |
| @@ -2803,7 +2823,7 @@ bool BitcoinMiner() | |||
| 2803 | } | 2823 | } |
| 2804 | } | 2824 | } |
| 2805 | pblock->nBits = nBits; | 2825 | pblock->nBits = nBits; |
| 2806 | pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nFees); | 2826 | pblock->vtx[0].vout[0].nValue = pblock->GetBlockValue(nBestHeight + 1, nFees); |
| 2807 | if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); | 2827 | if (LogAcceptsCategory("net")) printf("\n\nRunning BitcoinMiner with %d transactions in block\n", pblock->vtx.size()); |
| 2808 | 2828 | ||
| 2809 | 2829 | ||
| @@ -1073,7 +1073,7 @@ public: | |||
| 1073 | } | 1073 | } |
| 1074 | 1074 | ||
| 1075 | 1075 | ||
| 1076 | int64 GetBlockValue(int64 nFees) const; | 1076 | int64 GetBlockValue(int nHeight, int64 nFees) const; |
| 1077 | bool DisconnectBlock(CTxDB& txdb, CBlockIndex* pindex); | 1077 | bool DisconnectBlock(CTxDB& txdb, CBlockIndex* pindex); |
| 1078 | bool ConnectBlock(CTxDB& txdb, CBlockIndex* pindex); | 1078 | bool ConnectBlock(CTxDB& txdb, CBlockIndex* pindex); |
| 1079 | bool ReadFromDisk(const CBlockIndex* blockindex, bool fReadTransactions); | 1079 | bool ReadFromDisk(const CBlockIndex* blockindex, bool fReadTransactions); |
diff --git a/src/rpc.cpp b/src/rpc.cpp index 1b529e8..3b2dae9 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -182,7 +182,7 @@ static bool RebuildJob() | |||
| 182 | } | 182 | } |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | block.vtx[0].vout[0].nValue = block.GetBlockValue(0); | 185 | block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, 0); |
| 186 | block.hashPrevBlock = pindexPrev->GetBlockHash(); | 186 | block.hashPrevBlock = pindexPrev->GetBlockHash(); |
| 187 | block.hashMerkleRoot = block.BuildMerkleTree(); | 187 | block.hashMerkleRoot = block.BuildMerkleTree(); |
| 188 | block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), | 188 | block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), |
diff --git a/src/script.cpp b/src/script.cpp index d2111ed..44eed93 100644 --- a/src/script.cpp +++ b/src/script.cpp | |||
| @@ -67,6 +67,24 @@ bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nI | |||
| 67 | if (!script.GetOp(pc, opcode, vchPushValue)) | 67 | if (!script.GetOp(pc, opcode, vchPushValue)) |
| 68 | return false; | 68 | return false; |
| 69 | 69 | ||
| 70 | if (vchPushValue.size() > MAX_SCRIPT_ELEMENT_SIZE) | ||
| 71 | return false; | ||
| 72 | |||
| 73 | // Disabled opcodes. Checked before the fExec test, so they invalidate | ||
| 74 | // the script even inside a branch that is never taken. | ||
| 75 | // | ||
| 76 | // These are unbounded: OP_CAT grows a stack element without limit | ||
| 77 | // (<x> OP_DUP OP_CAT repeated doubles it each time, so a tiny script | ||
| 78 | // exhausts memory), and OP_LSHIFT shifts by an attacker-supplied | ||
| 79 | // amount with no ceiling. Both let any transaction take down every | ||
| 80 | // node that validates it. Bitcoin removed the whole group for this. | ||
| 81 | if (opcode == OP_CAT || opcode == OP_SUBSTR || opcode == OP_LEFT || | ||
| 82 | opcode == OP_RIGHT || opcode == OP_INVERT || opcode == OP_AND || | ||
| 83 | opcode == OP_OR || opcode == OP_XOR || opcode == OP_2MUL || | ||
| 84 | opcode == OP_2DIV || opcode == OP_MUL || opcode == OP_DIV || | ||
| 85 | opcode == OP_MOD || opcode == OP_LSHIFT || opcode == OP_RSHIFT) | ||
| 86 | return false; | ||
| 87 | |||
| 70 | if (fExec && opcode <= OP_PUSHDATA4) | 88 | if (fExec && opcode <= OP_PUSHDATA4) |
| 71 | stack.push_back(vchPushValue); | 89 | stack.push_back(vchPushValue); |
| 72 | else if (fExec || (OP_IF <= opcode && opcode <= OP_ENDIF)) | 90 | else if (fExec || (OP_IF <= opcode && opcode <= OP_ENDIF)) |
| @@ -169,9 +187,18 @@ bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nI | |||
| 169 | 187 | ||
| 170 | case OP_RETURN: | 188 | case OP_RETURN: |
| 171 | { | 189 | { |
| 172 | pc = pend; | 190 | // Must fail the script outright, never just stop executing it. |
| 191 | // | ||
| 192 | // scriptSig and scriptPubKey are evaluated as one concatenated | ||
| 193 | // script (see VerifySignature). The 0.1.0 behaviour here was | ||
| 194 | // "pc = pend", which jumps past everything that follows — so a | ||
| 195 | // scriptSig of <OP_TRUE OP_RETURN> skipped the entire | ||
| 196 | // scriptPubKey, including OP_CHECKSIG, and the final | ||
| 197 | // CastToBool(stack.back()) saw the attacker's own OP_TRUE. | ||
| 198 | // That let anyone spend anyone else's outputs without a | ||
| 199 | // signature. Bitcoin closed this in 0.3.5. | ||
| 200 | return false; | ||
| 173 | } | 201 | } |
| 174 | break; | ||
| 175 | 202 | ||
| 176 | 203 | ||
| 177 | // | 204 | // |
diff --git a/src/script.h b/src/script.h index d52d06b..27c1b13 100644 --- a/src/script.h +++ b/src/script.h | |||
| @@ -4,6 +4,10 @@ | |||
| 4 | 4 | ||
| 5 | class CTransaction; | 5 | class CTransaction; |
| 6 | 6 | ||
| 7 | // Largest byte vector a script may push onto the stack. Without a ceiling, | ||
| 8 | // script evaluation allocates whatever the transaction asks it to. | ||
| 9 | static const unsigned int MAX_SCRIPT_ELEMENT_SIZE = 520; | ||
| 10 | |||
| 7 | enum | 11 | enum |
| 8 | { | 12 | { |
| 9 | SIGHASH_ALL = 1, | 13 | SIGHASH_ALL = 1, |