upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-28 13:58:43 -0300
committerGitHub <noreply@github.com>2026-07-28 13:58:43 -0300
commit0ddcf690bf48075927f9d7596c62ddb41e79b2e0 (patch)
treea9a97ae83a52b449047d070b1b333d5d15c67490
parentb8b6efd7b4fcaf692a276ac4e4e08fb2b0e3bfc0 (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.
-rw-r--r--src/main.cpp30
-rw-r--r--src/main.h2
-rw-r--r--src/rpc.cpp2
-rw-r--r--src/script.cpp31
-rw-r--r--src/script.h4
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
732int64 CBlock::GetBlockValue(int64 nFees) const 732int64 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
diff --git a/src/main.h b/src/main.h
index 8bb8609..533974c 100644
--- a/src/main.h
+++ b/src/main.h
@@ -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
5class CTransaction; 5class 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.
9static const unsigned int MAX_SCRIPT_ELEMENT_SIZE = 520;
10
7enum 11enum
8{ 12{
9 SIGHASH_ALL = 1, 13 SIGHASH_ALL = 1,